package com.m3958.encode.detector;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;

import com.m3958.encode.detector.exception.FileOpenException;
import com.m3958.encode.detector.util.ByteUtil;

public abstract class AbstractDetector implements Detector {
    
    public static final byte ZERO = 0x0;
    public static final byte ONE27 = 0x7F;
    
    private byte[] bytes;
    
    private Path path;
    
    private DetectResult result;
    
    private boolean entireFile;
    
    protected int successedNumber = 0;
    protected int failedNumber = 0;

    protected int asciiNumber = 0;
    protected int charNumber = 0;
    
    private byte[] remainBytes;
    
    public AbstractDetector(){}
    
    public AbstractDetector(byte[] bytes) {
        this.bytes = bytes;
    }
    
    public AbstractDetector(Path path) {
        this.path = path;
    }
    
    protected boolean isAscii(byte b) {
        if (b == ZERO || b == ONE27) {
            return true;
        }
        return b > ZERO && b < ONE27;
    }
    
    public Detector scanWholeFile() {
        entireFile = true;
        return this;
    }

    @Override
    public DetectResult result() {
        return result;
    }

    @Override
    public Detector detect() throws FileOpenException {
        Detector d;
        if (bytes == null) {
            d = dfile();
        } else {
            d = dbytes(bytes);            
        }
        result = new DetectResult(getCharsetName(), asciiNumber, charNumber, successedNumber, failedNumber);
        return d;
    }
    
    protected abstract String getCharsetName();
    
    protected abstract LanguageName getLanguageName();

    private Detector dfile() throws FileOpenException {
        SeekableByteChannel seekableBc = null;
        try {
            
            seekableBc = Files.newByteChannel(path);
            ByteBuffer bb = ByteBuffer.allocate(1000);
            
            int readed = 0;
            
            while (readed != -1) {
                do {
                    readed = seekableBc.read(bb); 
                } while(readed != -1 && bb.hasRemaining());
                bb.flip();
                byte[] ba = new byte[bb.limit()];
                bb.get(ba);
                dbytes(ba);
                bb.clear();
                if (!entireFile) {
                    break;
                }
            }
            return this;
        } catch (IOException e) {
            throw new FileOpenException(path.toString());
        } finally {
            if (seekableBc != null) {
                try {
                    seekableBc.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private Detector dbytes(byte[] bytes) {
        if (remainBytes != null && remainBytes.length > 0) {
            bytes = ByteUtil.concat(remainBytes, bytes);
        }
        
        int i = 0;
        int len = bytes.length;
       
        for (; i < len;) {
            int j = 0;
            byte[] workingBytes = new byte[0];
            for(;j < maxCharBytes();j++) {
                if (i < len) {
                    byte[] nbytes = new byte[workingBytes.length + 1];
                    System.arraycopy(workingBytes, 0, nbytes, 0, workingBytes.length);
                    nbytes[nbytes.length - 1] = bytes[i++];
                    workingBytes = nbytes;
                    int v = detectOne(workingBytes);
                    if (v == 0) {
                        workingBytes = new byte[0];
                        break;
                    }
                    if (i == len && v == 1) {
                        remainBytes = workingBytes;
                    } else {
                        remainBytes = null;
                    }
                } else {
                    break;
                }
            }
        }
        return this;
    }
    
    
    
    /**
     * 
     * @param bytes
     * @return 0: one char done(don't care right or wrong), 1:need more bytes. 2: it is, -2: it is not!
     */
    protected abstract int detectOne(byte...bytes);
    
    protected abstract int maxCharBytes();

    public byte[] getBytes() {
        return bytes;
    }

    public void setBytes(byte[] bytes) {
        this.bytes = bytes;
    }

    public Path getPath() {
        return path;
    }

    public void setPath(Path path) {
        this.path = path;
    }
}
